-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local log = require 'mc.logging'
local cmn = require 'common'
local ctx = require 'mc.context'
local ipmi = require 'ipmi'
local enums = require 'ipmi.enums'
local bs = require 'mc.bitstring'
local channel_type = enums.ChannelType
local comp_code = ipmi.types.Cc

local c_ocp_card = class()

local RET_OK <const> = 0
local RET_ERR <const> = 1
local CAP_ID_INDEX <const> = 1
local ADDR_INDEX <const> = 2
local PCI_CAP_ID_EXP <const> = 0x10
local PCIE_LINK_CAP_REG_OFFSET <const> = 0x0C -- pcie link capabilities寄存器偏移
local PCIE_LINK_CONTROL_OFFSET <const> = 0x10 -- pcie link control寄存器偏移
local PCIE_LINK_WIDTH_OFFSET <const> = 4
local PCIE_LINK_WIDTH_MASK <const> = 0x3F
local PCIE_CAP_ID_OFFSET <const> = 0x34
local DEFAULT_INTERFACE <const> = 'bmc.kepler.Systems.PCIeDevices.PCIeCard'
local MAX_CHECK_COUNT<const> = 3

function c_ocp_card:get_prop(name, interface)
    if not self.mds then
        return nil
    end

    if interface then
        return self.mds[interface][name]
    elseif self.mds[name] then
        return self.mds[name]
    else
        return self.mds[DEFAULT_INTERFACE][name]
    end
end

function c_ocp_card:set_prop(name, val, interface)
    if not self.mds then
        return nil
    end

    if interface then
        self.mds[interface][name] = val
    elseif self.mds[name] then
        self.mds[name] = val
    else
        self.mds[DEFAULT_INTERFACE][name] = val
    end
end

local get_info_from_pmu_req = bs.new('<<0xDB0700:3/unit:8, cpu_num,' ..
                                     'pci_addr_1:1/unit:8,' ..
                                     'pci_addr_2:1/unit:8,' ..
                                     'pci_addr_3:1/unit:8,' ..
                                     'pci_addr_4:1/unit:8,' ..
                                     'read_length>>')
local get_info_from_pmu_rsp = bs.new('<<0xDB0700:3/unit:8, info_byte:1/unit:8, tail/binary>>')
local get_info_per_byte = bs.new('<<info_byte:1/unit:8, tail/binary>>')

local links_table <const> = {
    [1] = 'X1', --PCIe卡带宽 X1
    [2] = 'X2', --PCIe卡带宽 X2
    [4] = 'X4', --PCIe卡带宽 X4
    [8] = 'X8', --PCIe卡带宽 X8
    [12] = 'X12', --PCIe卡带宽 X12
    [16] = 'X16' --PCIe卡带宽 X16
}

local LINKS_SPEED_TABLE <const> = {
    [1] = '2.5GT/s', --GEN1
    [2] = '5.0GT/s', --GEN2
    [3] = '8.0GT/s', --GEN3
    [4] = '16.0GT/s', --GEN4
    [5] = '32.0GT/s',--GEN5
    [6] = 'RsvdP',--RsvdP
    [7] = 'RsvdP',--RsvdP
}

local function link_speed_to_str(link_speed)
    if LINKS_SPEED_TABLE[link_speed] then
        return LINKS_SPEED_TABLE[link_speed]
    end
    log:error("link_speed_to_str:faild to link speed to string(%d).", link_speed)
    return 'N/A'
end

local function link_width_to_str(link_width)
    if links_table[link_width] then
        return links_table[link_width]
    end
    log:error("link_width_to_str:faild to link width to string(%d).", link_width)
    return 'N/A'
end

-- 解析ipmi返回消息中的PCI信息
local function parse_info_from_pmu(payload, length)
    local rsp = get_info_from_pmu_rsp:unpack(payload)
    local info_byte = rsp.info_byte
    local tail = rsp.tail
    local i = 1
    local result_arr = {}
    local read_loop
    result_arr[#result_arr + 1] = info_byte
    -- 根据指定长度，挨个读取ipmi返回数据中的字节
    while i < length and tail ~= nil and #tail ~= 0 do
        read_loop = get_info_per_byte:unpack(tail)
        info_byte, tail = read_loop.info_byte, read_loop.tail
        result_arr[#result_arr + 1] = info_byte
        i = i + 1
    end
    return result_arr
end

local function get_pcie_info_from_ipmi(bus, pcie_info)
    if pcie_info == nil then
        log:error('pcie_info is nil')
        return
    end
    local cpu_id = 0
    if pcie_info.is_local == 1 then
        cpu_id = (pcie_info.cpu_id & 0x07) | 0x40
    else
        cpu_id = pcie_info.cpu_id & 0x07
    end
    local addr = {}
    addr[1] = pcie_info.address & 0xff
    addr[2] = ((pcie_info.function_num & 0x07) << 4) | ((pcie_info.device_num & 0x01) << 7)
    addr[3] = ((pcie_info.device_num >> 1) & 0x0f) | ((pcie_info.bus_num & 0x0f) << 4)
    addr[4] = pcie_info.bus_num >> 4
    local length = pcie_info.read_length == 4 and 3 or 4
    local req_data = get_info_from_pmu_req:pack({cpu_num = cpu_id, pci_addr_1 = addr[1], pci_addr_2 = addr[2],
                                                pci_addr_3 = addr[3], pci_addr_4 = addr[4], read_length = length})
    local result, payload = ipmi.request(bus, channel_type.CT_ME:value(), {DestNetFn = 0x2e, Cmd = 0x44, Payload = req_data})
    if result == comp_code.Success then
        local info_arr = parse_info_from_pmu(payload, pcie_info.read_length)
        log:info('get pci register info successfully')
        return info_arr
    end
    log:debug('get pci register info failed, error: %s', result)
    return
end

function c_ocp_card:pcie_get_capid_addr(info, cap_id)
    info['address'] = PCIE_CAP_ID_OFFSET
    local reg_value = get_pcie_info_from_ipmi(self.bus, info)
    if not reg_value then
        log:debug("pcie_get_capid_addr: failed to get first cap id.")
        return
    end
    local success_flag = false
    info['address'] = reg_value[CAP_ID_INDEX]
    for _ = 1, 20 do -- 进行20次cap id匹配
        reg_value = get_pcie_info_from_ipmi(self.bus, info)
        if not reg_value then
            log:error("pcie_get_capid_addr: failed to get reg address.")
            return
        end

        if reg_value[CAP_ID_INDEX] ~= cap_id then
            if not reg_value[ADDR_INDEX] or reg_value[ADDR_INDEX] == 0 then
                log:error("pcie_get_capid_addr: not find cap id(%d).", cap_id)
                return
            end
            info['address'] = reg_value[ADDR_INDEX]
            goto CONTINUE
        end
        success_flag = true
        break
        ::CONTINUE::
    end
    if success_flag then
        return info['address']
    end
    return
end

local prop_map = {}

local function monitor_pcie_info(name, value, sender)
    if prop_map[name] then
        prop_map[name](value)
        return
    end
end

prop_map['MultihostPresence'] = function (value, obj)
    local card_for_servers = {}
    local cur_sys_id = 1
    while value > 0 do
        if value & 1 > 0 then
            table.insert(card_for_servers, cur_sys_id)
            log:debug('[sync_info_to_card], card_for_servers add system id=%s, position=%s',
                cur_sys_id, obj.position)
        end
        value = value >> 1
        cur_sys_id = cur_sys_id + 1
    end
    obj:set_prop('CardForServers', card_for_servers)
end

-- 从PCIeDevice同步属性到PCIeCard
function c_ocp_card:sync_info_to_card(pcie_device_obj)
    -- 注册属性变化的监听
    pcie_device_obj.mds.property_changed:on(monitor_pcie_info)
    -- 同步一次属性值
    local value
    local ok, err
    for device_prop, card_prop in pairs(prop_map) do
        value = pcie_device_obj:get_prop(device_prop)
        log:debug('sync_info_to_card, name=%s, value=%s', device_prop, value)
        ok, err = pcall(function()
            prop_map[device_prop](value, self)
        end)
        if not ok then
            log:warn('sync_info_to_card, not ok, err=%s', err)
        end
    end
end

function c_ocp_card:get_pcie_lang_info(pcie_device_obj)
    self.pcie_info.cpu_id = pcie_device_obj:get_prop('SocketID')
    self.pcie_info.bus_num = pcie_device_obj:get_prop('DevBus')
    self.pcie_info.device_num = pcie_device_obj:get_prop('DevDevice')
    self.pcie_info.function_num = pcie_device_obj:get_prop('DevFunction')
    local pcie_info = self.pcie_info
    local addr = self:pcie_get_capid_addr(pcie_info, PCI_CAP_ID_EXP)
    if not addr then
        log:debug("get_pcie_lang_info: get cap id addr failed.")
        return RET_ERR
    end
    pcie_info['address'] = addr + PCIE_LINK_CAP_REG_OFFSET
    local ret = get_pcie_info_from_ipmi(self.bus, pcie_info)
    if not ret then
        log:error("get_pcie_lang_info: get link cap reg value failed")
        return RET_ERR
    end
    local link_width_ability = ((ret[1] >> PCIE_LINK_WIDTH_OFFSET) + (ret[2] << PCIE_LINK_WIDTH_OFFSET))
        & PCIE_LINK_WIDTH_MASK
    self.mds['LinkWidthAbility'] = link_width_to_str(link_width_ability)

    local link_speed_capability = ret[1] & 0xF
    pcie_info['address'] = addr + PCIE_LINK_CONTROL_OFFSET
    ret = get_pcie_info_from_ipmi(self.bus, pcie_info)
    if not ret then
        log:error("get_pcie_lang_info: get link status reg value failed")
        return RET_ERR
    end
    local link_width = ((ret[3] >> PCIE_LINK_WIDTH_OFFSET) + (ret[4] << PCIE_LINK_WIDTH_OFFSET))
        & PCIE_LINK_WIDTH_MASK
    self.mds['LinkWidth'] = link_width_to_str(link_width)

    local link_speed = ret[3] & 0xF
    local ok, err = pcall(function ()
        self.mds['LinkSpeed'] = link_speed_to_str(link_speed) 
    end)
    if not ok then
        log:debug("[pcie_device] link_speed_to_str link_speed error:%s", err)
    end

    local ok, err = pcall(function ()
        self.mds['LinkSpeedCapability'] = link_speed_to_str(link_speed_capability)
    end)
    if not ok then
        log:debug("[pcie_device] link_speed_to_str link_speed_capability error:%s", err)
    end
    return RET_OK
end

-- 数字转换为PcbVersion的格式
local function pcbid_to_pcbver(pcbid)
    if type(pcbid) ~= 'number' or (pcbid < 1 or pcbid > 26) then -- PcbID的合法取值为1-26
        log:error('pcbid_to_pcbver: The pcbid %s is invalid.', pcbid)
        return nil
    end
    return '.' .. string.char(pcbid + 64) -- 64为字符'A'的前一个字符对应的ASCII码
end

-- 更新pcbversion字段的任务，因为刚加载时会读取不到pcb版本，因此要启动线程做重试处理
function c_ocp_card:task_update_pcb_version()
    if self.fetch_pcb_flag == false then
        return
    end
    cmn.skynet.fork(function ()
        local timeout = 120
        local version = nil
        local pcbid
        repeat
            pcbid = self:get_prop('PcbID')
            if pcbid and pcbid ~= 0 then
                version = pcbid_to_pcbver(pcbid)
                if version then
                    self:set_prop('PcbVersion', version)
                end
            end
            cmn.skynet.sleep(200)
            timeout = timeout - 1
        until version or timeout <= 0
        if version == nil then
            self.fetch_pcb_flag = false
            log:error('Get PCB Version failed')
        end
    end)
end

-- 使用pcie_device的bdf，防止因PCIeCard属性未同步完成而获取到错误的bdf信息
function c_ocp_card:update_pcie_vpd_info(pcie_device_obj)
    local local_db_handle = self.reset_local_db
    local tbl = local_db_handle.PCIeVPDInfo
    local dev_bus = pcie_device_obj:get_prop('DevBus')
    local dev_device = pcie_device_obj:get_prop('DevDevice')
    local dev_function = pcie_device_obj:get_prop('DevFunction')
    local record = local_db_handle:select(tbl):where({DevBus = dev_bus,
        DevDevice = dev_device, DevFunction = dev_function}):first()

    if not record then
        -- 没有VPD记录，直接返回
        log:notice('no VPD info for DevBDF(%s/%s/%s)', dev_bus, dev_device,
            dev_function)
        return
    end

    local ok, err
    if record.SerialNumber then
        ok, err = pcall(function()
            return self:set_prop('SerialNumber', record.SerialNumber)
        end)
        if not ok then
            log:error('update SerialNumber error: %s', err)
        end
    end

    if record.PartNumber then
        ok, err = pcall(function()
            return self:set_prop('PartNumber', record.PartNumber)
        end)
        if not ok then
            log:error('update PartNumber error: %s', err)
        end
    end
end

function c_ocp_card:update_vid_did(pcie_addr_obj)
    self:set_prop('VendorID', pcie_addr_obj:get_prop('VendorID'))
    self:set_prop('DeviceID', pcie_addr_obj:get_prop('DeviceID'))
end

function c_ocp_card:update_description(v)
    self:set_prop('Description', v)
end

function c_ocp_card:add_check_count()
    self.check_count = self.check_count + 1
end

function c_ocp_card:need_check()
    if self.check_count >= MAX_CHECK_COUNT then
        return false
    end
    return true
end

function c_ocp_card:init()
    self.fetch_pcb_flag = true
    self:task_update_pcb_version()
end

function c_ocp_card:ctor(mds_obj, position, bus, reset_local_db)
    self.mds = mds_obj
    self.position = position
    self.bus = bus
    self.reset_local_db = reset_local_db
    self.pcie_info = {
        cpu_id = 0,
        is_local = 0,
        bus_num = 0,
        device_num = 0,
        function_num = 0,
        address = 0,
        read_length = 4
    }
    self.check_count = 0
end

return c_ocp_card